#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/shape.h>
#include <X11/xpm.h>
#include <errno.h>
#include <freetype2/ft2build.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <sys/select.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include FT_FREETYPE_H
#include FT_GLYPH_H

#define VERSION "v0.11 (FreeType2)"
#define ABS(a) ((a) < 0 ? -(a) : (a))
#define CLAMP(v, l, h) ((v) < (l) ? (l) : ((v) > (h) ? (h) : (v)))

// Default config values
#define DEFAULT_PET_SPEED 2
#define DEFAULT_PET_REFRESH 24
#define DEFAULT_FRAME_DURATION 200
#define DEFAULT_SCALE_FACTOR 10
#define DEFAULT_ORIGINAL_SIZE 64
#define DEFAULT_WANDER_MIN_WAIT 16000
#define DEFAULT_WANDER_MAX_WAIT 32000
#define DEFAULT_WANDER_MARGIN 100
#define DEFAULT_SLEEP_DELAY 5000
#define DEFAULT_HAPPY_DURATION 3000
#define DEFAULT_SPEECH_DURATION 3000
#define DEFAULT_PET_ASSET_DIR "./pets/dog"
#define DEFAULT_FONT_PATH "/usr/share/fonts/truetype/freefont/FreeSans.ttf"
#define DEFAULT_FONT_SIZE 24

// Config file paths
#define CONFIG_DIR "~/.config/xpet"
#define CONFIG_PATH CONFIG_DIR "/config"

// Key function enums
typedef enum {
  FUNC_TOGGLE_CHASE,
  FUNC_TOGGLE_FREEZE,
  FUNC_QUIT,
  FUNC_PRINT_HELLOWORLD,
  // TODO: add more function here
  FUNC_UNKNOWN,
} KeyFunc;

enum state {
  SLEEPING,
  IDLE,
  N,
  S,
  E,
  W,
  NW,
  NE,
  SW,
  SE,
  DRAGGED,
  HAPPY,
  STATE_LAST
};

struct frame {
  Pixmap pix;
  Pixmap mask;
  int duration;
};
struct animation {
  struct frame *frames;
  const char *name;
  int *frame_durations;
  int n_frames;
  Bool loop;
};
struct mouse {
  int x;
  int y;
};
struct keybind {
  KeySym sym;
  unsigned long mask;
  KeyFunc func;
};

// Config struct with FreeType2 settings
typedef struct {
  char **phrases;
  int phrase_count;
  int scale_factor;
  int pet_speed;
  int frame_duration;
  char *pet_asset_dir;
  char *font_path; // FreeType2 font path
  int font_size;   // Font size (pixels)
  struct keybind *keybinds;
  int keybind_count;
} Config;

struct pet {
  Window window;
  Bool chasing;
  Bool frozen;
  enum state state;
  int x;
  int y;
  double subpixel_x;
  double subpixel_y;
  struct animation *current_animation;
  int current_frame;
  long frame_time;
  int target_x;
  int target_y;
  long wander_wait;
  long frozen_time;
  enum state previous_state;
  long happy_time;
  Bool dragging;
  int drag_offset_x;
  int drag_offset_y;
  Bool was_chasing;
  Bool was_frozen;
  const char *speech;
  long speech_time;
  Window bubble_window;
};

// Global variables
Config config;
struct mask_map {
  const char *name;
  unsigned long mask;
};
const struct mask_map MASK_MAP[] = {{"Mod1Mask", Mod1Mask},
                                    {"Mod2Mask", Mod2Mask},
                                    {"Mod3Mask", Mod3Mask},
                                    {"Mod4Mask", Mod4Mask},
                                    {"Mod5Mask", Mod5Mask},
                                    {"ShiftMask", ShiftMask},
                                    {"LockMask", LockMask},
                                    {"ControlMask", ControlMask},
                                    {NULL, 0}};
struct func_map {
  const char *name;
  KeyFunc func;
};
const struct func_map FUNC_MAP[] = {{"toggle_chase", FUNC_TOGGLE_CHASE},
                                    {"toggle_freeze", FUNC_TOGGLE_FREEZE},
                                    {
                                        "quit",
                                        FUNC_QUIT,
                                    },
                                    {
                                        "hello_world",
                                        FUNC_PRINT_HELLOWORLD,
                                    },
                                    {NULL, FUNC_UNKNOWN}};
struct animation animations[] = {
    [HAPPY] = {.name = "happy",
               .n_frames = 6,
               .loop = True,
               .frames = NULL,
               .frame_durations = NULL},
    [SLEEPING] = {.name = "sleeping",
                  .n_frames = 6,
                  .loop = True,
                  .frames = NULL,
                  .frame_durations = NULL},
    [IDLE] = {.name = "idle",
              .n_frames = 6,
              .loop = True,
              .frames = NULL,
              .frame_durations = NULL},
    [DRAGGED] = {.name = "dragged",
                 .n_frames = 6,
                 .loop = True,
                 .frames = NULL,
                 .frame_durations = NULL},
    [N] = {.name = "walk_north",
           .n_frames = 2,
           .loop = True,
           .frames = NULL,
           .frame_durations = NULL},
    [S] = {.name = "walk_south",
           .n_frames = 2,
           .loop = True,
           .frames = NULL,
           .frame_durations = NULL},
    [E] = {.name = "walk_east",
           .n_frames = 2,
           .loop = True,
           .frames = NULL,
           .frame_durations = NULL},
    [W] = {.name = "walk_west",
           .n_frames = 2,
           .loop = True,
           .frames = NULL,
           .frame_durations = NULL},
    [NW] = {.name = "walk_northwest",
            .n_frames = 2,
            .loop = True,
            .frames = NULL,
            .frame_durations = NULL},
    [NE] = {.name = "walk_northeast",
            .n_frames = 2,
            .loop = True,
            .frames = NULL,
            .frame_durations = NULL},
    [SW] = {.name = "walk_southwest",
            .n_frames = 2,
            .loop = True,
            .frames = NULL,
            .frame_durations = NULL},
    [SE] = {.name = "walk_southeast",
            .n_frames = 2,
            .loop = True,
            .frames = NULL,
            .frame_durations = NULL},
};
Display *dpy;
Window root;
int scr;
int scr_width;
int scr_height;
XpmAttributes xpm_attrs;
FT_Library ft_lib; // FreeType2 library
FT_Face ft_face;   // FreeType2 font face
struct mouse mouse;
struct pet pet;

// Helper: Trim whitespace from string
char *trim_whitespace(char *str) {
  if (!str)
    return NULL;
  // Trim leading
  while (*str && (*str == ' ' || *str == '\t' || *str == '\n' || *str == '\r'))
    str++;
  // Trim trailing
  char *end = str + strlen(str) - 1;
  while (end > str &&
         (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r'))
    end--;
  *(end + 1) = '\0';
  return str;
}

// Expand ~ to home directory
char *expand_tilde(const char *path) {
  if (!path)
    return NULL;
  if (path[0] == '~') {
    char *home = getenv("HOME");
    if (!home)
      return strdup(path);
    char *expanded = malloc(strlen(home) + strlen(path) + 1);
    snprintf(expanded, strlen(home) + strlen(path) + 1, "%s%s", home, path + 1);
    return expanded;
  }
  return strdup(path);
}

// Create directory recursively
int mkdir_p(const char *path, mode_t mode) {
  char dir[PATH_MAX];
  strncpy(dir, path, sizeof(dir));
  size_t len = strlen(dir);
  if (dir[len - 1] == '/')
    dir[len - 1] = '\0';
  char *p = dir + 1;
  while (*p) {
    if (*p == '/') {
      *p = '\0';
      if (mkdir(dir, mode) != 0 && errno != EEXIST)
        return -1;
      *p = '/';
    }
    p++;
  }
  return mkdir(dir, mode) != 0 && errno != EEXIST ? -1 : 0;
}

// Generate default config with user's structure
void generate_default_config(const char *config_path) {
  FILE *f = fopen(config_path, "w");
  if (!f) {
    perror("generate default config");
    return;
  }

  // Phrases
  fprintf(f, "phrase = \"Get off the chair, you lazybones～\"\n");
  fprintf(f, "phrase = \"Oh wow, you’re too chubby—time to exercise～\"\n");
  fprintf(f,
          "phrase = \"Woof! A stranger’s approaching? I’ll protect you!\"\n");
  fprintf(f, "phrase = \"Woof! Wanna play fetch? Throw the ball already～\"\n");
  fprintf(f, "phrase = \"The weather’s great today—wanna go for a walk?\"\n");
  fprintf(f, "phrase = \"Whimper～ I’m kinda hungry, need foodies!\"\n");
  fprintf(f, "phrase = \"Master, play with me please? I’m so bored～\"\n");
  fprintf(f, "phrase = \"I like you so much! I’ll always follow you～\"\n\n");

  // Font settings (FreeType2)
  fprintf(f,
          "font_path = \"/usr/share/fonts/truetype/freefont/FreeSans.ttf\"\n");
  fprintf(f, "font_size = 24\n\n");

  // Base config
  fprintf(f, "scale_factor = 10\n");
  fprintf(f, "pet_speed = 2\n");
  fprintf(f, "frame_duration = 200\n");
  fprintf(f, "pet_asset_dir = \"./pets/dog\"\n\n");

  // Comments
  fprintf(f, "# Comments: lines starting with # are ignored\n");
  fprintf(f, "# Built-in functions: toggle_chase, toggle_freeze, quit\n\n");

  // Keybinds
  fprintf(f, "keybind ControlMask+f toggle_chase\n");
  fprintf(f, "keybind ControlMask+s toggle_freeze\n");
  fprintf(f, "keybind Mod1Mask+q quit\n");

  fclose(f);
  printf("Generated default config: %s\n", config_path);
}

// Convert mask string to X11 mask
unsigned long mask_str_to_mask(const char *mask_str) {
  for (int i = 0; MASK_MAP[i].name; i++)
    if (strcmp(mask_str, MASK_MAP[i].name) == 0)
      return MASK_MAP[i].mask;
  fprintf(stderr, "Unknown modifier '%s'—using Mod1Mask\n", mask_str);
  return Mod1Mask;
}

// Convert function string to enum
KeyFunc func_str_to_func(const char *func_str) {
  for (int i = 0; FUNC_MAP[i].name; i++)
    if (strcmp(func_str, FUNC_MAP[i].name) == 0)
      return FUNC_MAP[i].func;
  fprintf(stderr, "Unknown function '%s'—ignoring\n", func_str);
  return FUNC_UNKNOWN;
}

// Parse "Modifier+Key" string (e.g., "ControlMask+f")
void parse_key_str(const char *key_str, char **mask_str, char **sym_str) {
  char *plus_pos = strchr(key_str, '+');
  if (!plus_pos) {
    *mask_str = strdup("0");
    *sym_str = strdup(key_str);
  } else {
    *mask_str = strndup(key_str, plus_pos - key_str);
    *sym_str = strdup(plus_pos + 1);
  }
}

// Check if directory exists
int check_asset_dir(const char *dir) {
  struct stat st;
  return stat(dir, &st) == 0 && S_ISDIR(st.st_mode);
}

// Check if font file exists
int check_font_file(const char *path) {
  struct stat st;
  return stat(path, &st) == 0 && S_ISREG(st.st_mode);
}

// Parse config file with proper handling of =, quotes, and comments
void parse_config(const char *config_path) {
  // Initialize defaults
  config.phrase_count = 0;
  config.phrases = malloc(sizeof(char *));
  config.phrases[0] = NULL;
  config.scale_factor = DEFAULT_SCALE_FACTOR;
  config.pet_speed = DEFAULT_PET_SPEED;
  config.frame_duration = DEFAULT_FRAME_DURATION;
  config.pet_asset_dir = expand_tilde(DEFAULT_PET_ASSET_DIR);
  config.font_path = expand_tilde(DEFAULT_FONT_PATH);
  config.font_size = DEFAULT_FONT_SIZE;
  config.keybinds = NULL;
  config.keybind_count = 0;

  FILE *f = fopen(config_path, "r");
  if (!f) {
    perror("open config");
    return;
  }

  char line[1024];
  while (fgets(line, sizeof(line), f)) {
    // Remove newline
    line[strcspn(line, "\n\r")] = '\0';
    // Skip empty lines
    if (line[0] == '\0')
      continue;
    // Skip comments (lines starting with #)
    if (strchr(" \t", line[0])) {
      char *first_non_whitespace = line + strspn(line, " \t");
      if (first_non_whitespace[0] == '#')
        continue;
    } else if (line[0] == '#')
      continue;

    // Parse keybind (special format: keybind <mod+key> <func>)
    if (strstr(line, "keybind ") == line) {
      char *key_str = strtok(line + 7, " \t"); // Skip "keybind "
      char *func_str = strtok(NULL, " \t");
      if (!key_str || !func_str)
        continue;

      char *mask_str = NULL;
      char *sym_str = NULL;
      parse_key_str(key_str, &mask_str, &sym_str);

      unsigned long mask =
          (strcmp(mask_str, "0") == 0) ? 0 : mask_str_to_mask(mask_str);
      KeySym sym = XStringToKeysym(sym_str);
      KeyFunc func = func_str_to_func(func_str);

      if (sym != NoSymbol && func != FUNC_UNKNOWN) {
        config.keybinds =
            realloc(config.keybinds,
                    sizeof(struct keybind) * (config.keybind_count + 1));
        config.keybinds[config.keybind_count] =
            (struct keybind){sym, mask, func};
        config.keybind_count++;
      }
      free(mask_str);
      free(sym_str);
      continue;
    }

    // Parse key = value (split at first '=')
    char *eq_pos = strchr(line, '=');
    if (!eq_pos)
      continue; // Not a key=value line

    // Split into key and value
    *eq_pos = '\0'; // Split line at '='
    char *key = trim_whitespace(line);
    char *value = trim_whitespace(eq_pos + 1); // Value is after '='

    // Remove quotes from value
    if (value[0] == '"' && value[strlen(value) - 1] == '"') {
      value[strlen(value) - 1] = '\0';
      value++;
    }

    // Ignore empty key/value
    if (!key || !value || *key == '\0' || *value == '\0')
      continue;

    // Process key/value
    if (strcmp(key, "phrase") == 0) {
      config.phrases =
          realloc(config.phrases, sizeof(char *) * (config.phrase_count + 2));
      config.phrases[config.phrase_count] = strdup(value);
      config.phrases[config.phrase_count + 1] = NULL;
      config.phrase_count++;
    } else if (strcmp(key, "scale_factor") == 0) {
      config.scale_factor = atoi(value);
      if (config.scale_factor < 1)
        config.scale_factor = DEFAULT_SCALE_FACTOR;
    } else if (strcmp(key, "pet_speed") == 0) {
      config.pet_speed = atoi(value);
      if (config.pet_speed < 1)
        config.pet_speed = DEFAULT_PET_SPEED;
    } else if (strcmp(key, "frame_duration") == 0) {
      config.frame_duration = atoi(value);
      if (config.frame_duration < 50)
        config.frame_duration = DEFAULT_FRAME_DURATION;
    } else if (strcmp(key, "pet_asset_dir") == 0) {
      free(config.pet_asset_dir);
      config.pet_asset_dir = expand_tilde(value);
    } else if (strcmp(key, "font_path") == 0) {
      free(config.font_path);
      config.font_path = expand_tilde(value);
    } else if (strcmp(key, "font_size") == 0) {
      config.font_size = atoi(value);
      if (config.font_size < 8)
        config.font_size = DEFAULT_FONT_SIZE;
    }
  }
  fclose(f);

  // Validate asset directory
  if (!config.pet_asset_dir || !check_asset_dir(config.pet_asset_dir)) {
    fprintf(stderr, "Invalid asset directory '%s'—using default: %s\n",
            config.pet_asset_dir ? config.pet_asset_dir : "NULL",
            DEFAULT_PET_ASSET_DIR);
    free(config.pet_asset_dir);
    config.pet_asset_dir = expand_tilde(DEFAULT_PET_ASSET_DIR);
  }

  // Validate font file
  if (!config.font_path || !check_font_file(config.font_path)) {
    fprintf(stderr, "Invalid font file '%s'—using default: %s\n",
            config.font_path ? config.font_path : "NULL", DEFAULT_FONT_PATH);
    free(config.font_path);
    config.font_path = expand_tilde(DEFAULT_FONT_PATH);
  }

  // Fallback keybinds
  if (config.keybind_count == 0) {
    fprintf(stderr, "No keybinds—using defaults\n");
    config.keybinds = malloc(sizeof(struct keybind) * 3);
    config.keybinds[0] =
        (struct keybind){XStringToKeysym("f"), ControlMask, FUNC_TOGGLE_CHASE};
    config.keybinds[1] =
        (struct keybind){XStringToKeysym("s"), ControlMask, FUNC_TOGGLE_FREEZE};
    config.keybinds[2] =
        (struct keybind){XStringToKeysym("q"), Mod1Mask, FUNC_QUIT};
    config.keybind_count = 3;
  }

  // Fallback phrases
  if (config.phrase_count == 0) {
    config.phrases[0] = strdup("Woof! Hello, master～");
    config.phrases[1] = NULL;
    config.phrase_count = 1;
  }
}

// Load config
void load_config(void) {
  char *config_dir = expand_tilde(CONFIG_DIR);
  char *config_path = expand_tilde(CONFIG_PATH);
  if (mkdir_p(config_dir, 0755) != 0) {
    perror("create config dir");
    free(config_dir);
    free(config_path);
    exit(1);
  }
  struct stat st;
  if (stat(config_path, &st) != 0)
    generate_default_config(config_path);
  parse_config(config_path);
  free(config_dir);
  free(config_path);
}

// Scale pixmap
Pixmap scale_pixmap(Display *dpy, Drawable src, int src_w, int src_h,
                    int scale) {
  int dest_w = src_w * scale;
  int dest_h = src_h * scale;
  Pixmap dest =
      XCreatePixmap(dpy, root, dest_w, dest_h, DefaultDepth(dpy, scr));
  GC gc = XCreateGC(dpy, dest, 0, NULL);
  XImage *src_img = XGetImage(dpy, src, 0, 0, src_w, src_h, AllPlanes, ZPixmap);
  if (src_img) {
    for (int y = 0; y < src_h; y++) {
      for (int x = 0; x < src_w; x++) {
        unsigned long pixel = XGetPixel(src_img, x, y);
        XSetForeground(dpy, gc, pixel);
        XFillRectangle(dpy, dest, gc, x * scale, y * scale, scale, scale);
      }
    }
    XDestroyImage(src_img);
  }
  XFreeGC(dpy, gc);
  return dest;
}

// Scale mask
Pixmap scale_mask(Display *dpy, Drawable src, int src_w, int src_h, int scale) {
  int dest_w = src_w * scale;
  int dest_h = src_h * scale;
  Pixmap dest = XCreatePixmap(dpy, root, dest_w, dest_h, 1);
  GC gc = XCreateGC(dpy, dest, 0, NULL);
  XSetForeground(dpy, gc, 0);
  XImage *src_img = XGetImage(dpy, src, 0, 0, src_w, src_h, AllPlanes, ZPixmap);
  if (src_img) {
    for (int y = 0; y < src_h; y++) {
      for (int x = 0; x < src_w; x++) {
        unsigned long pixel = XGetPixel(src_img, x, y) & 1;
        XSetForeground(dpy, gc, pixel);
        XFillRectangle(dpy, dest, gc, x * scale, y * scale, scale, scale);
      }
    }
    XDestroyImage(src_img);
  }
  XFreeGC(dpy, gc);
  return dest;
}

// Sleep in ms
void xsleep(long ms) {
  struct timeval tv = {ms / 1000, (int)((ms % 1000) * 1000)};
  select(0, 0, 0, 0, &tv);
}

// Check if state is walking
Bool walking(enum state s) {
  return s == N || s == S || s == E || s == W || s == NW || s == NE ||
         s == SW || s == SE;
}

// Calculate movement step
void step(enum state d, double *step_x, double *step_y) {
  const double s = config.pet_speed;
  const double ds = s * 0.7071067811865475;
  switch (d) {
  case E:
    *step_x = s;
    *step_y = 0;
    break;
  case W:
    *step_x = -s;
    *step_y = 0;
    break;
  case N:
    *step_x = 0;
    *step_y = -s;
    break;
  case S:
    *step_x = 0;
    *step_y = s;
    break;
  case NE:
    *step_x = ds;
    *step_y = -ds;
    break;
  case SE:
    *step_x = ds;
    *step_y = ds;
    break;
  case NW:
    *step_x = -ds;
    *step_y = -ds;
    break;
  case SW:
    *step_x = -ds;
    *step_y = ds;
    break;
  default:
    *step_x = 0;
    *step_y = 0;
    break;
  }
}

// Find movement direction
enum state find_octant(int delta_x, int delta_y) {
  int abs_delta_x = ABS(delta_x);
  int abs_delta_y = ABS(delta_y);
  if (abs_delta_x > abs_delta_y * 2)
    return delta_x > 0 ? E : W;
  if (abs_delta_y > abs_delta_x * 2)
    return delta_y > 0 ? S : N;
  return delta_x > 0 ? (delta_y < 0 ? NE : SE) : (delta_y < 0 ? NW : SW);
}

// Hide speech bubble
void hide_speech_bubble(void) {
  if (pet.bubble_window)
    XUnmapWindow(dpy, pet.bubble_window);
  pet.speech = NULL;
  pet.speech_time = 0;
}

// Get mouse position
void get_mouse_pos(void) {
  Window root_ret, child_ret;
  int x_ret, y_ret, winx_ret, winy_ret;
  unsigned int mask_ret;
  XQueryPointer(dpy, root, &root_ret, &child_ret, &x_ret, &y_ret, &winx_ret,
                &winy_ret, &mask_ret);
  mouse.x = x_ret;
  mouse.y = y_ret;
}

// Initialize FreeType2
int init_freetype() {
  if (FT_Init_FreeType(&ft_lib) != 0) {
    fprintf(stderr, "Failed to initialize FreeType2\n");
    return -1;
  }
  if (FT_New_Face(ft_lib, config.font_path, 0, &ft_face) != 0) {
    fprintf(stderr, "Failed to load font: %s\n", config.font_path);
    FT_Done_FreeType(ft_lib);
    return -1;
  }
  FT_Set_Pixel_Sizes(ft_face, 0, config.font_size); // Set font size (pixels)
  return 0;
}

// FreeType2: Get text dimensions (width/height)
void ft_get_text_extents(const char *text, int *width, int *height) {
  *width = 0;
  *height = 0;
  FT_GlyphSlot slot = ft_face->glyph;
  int max_ascent = 0, max_descent = 0;

  for (size_t i = 0; i < strlen(text); i++) {
    FT_UInt glyph_index = FT_Get_Char_Index(ft_face, text[i]);
    if (FT_Load_Glyph(ft_face, glyph_index, FT_LOAD_DEFAULT) != 0)
      continue;
    if (FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL) != 0)
      continue;

    *width += slot->advance.x >> 6; // Convert 26.6 fixed point to pixels
    max_ascent = MAX(max_ascent, slot->bitmap_top);
    max_descent = MAX(max_descent, slot->bitmap.rows - slot->bitmap_top);
  }

  *height = max_ascent + max_descent;
}

// FreeType2: Draw text to window
void ft_draw_text(Window win, GC gc, const char *text, int x, int y) {
  FT_GlyphSlot slot = ft_face->glyph;
  int pen_x = x;
  int pen_y = y;

  XSetForeground(dpy, gc, BlackPixel(dpy, scr)); // Text color: black

  for (size_t i = 0; i < strlen(text); i++) {
    FT_UInt glyph_index = FT_Get_Char_Index(ft_face, text[i]);
    if (FT_Load_Glyph(ft_face, glyph_index, FT_LOAD_DEFAULT) != 0)
      continue;
    if (FT_Render_Glyph(slot, FT_RENDER_MODE_NORMAL) != 0)
      continue;

    // Calculate position
    int x_pos = pen_x + slot->bitmap_left;
    int y_pos = pen_y - slot->bitmap_top;

    // Draw glyph bitmap
    for (int row = 0; row < slot->bitmap.rows; row++) {
      for (int col = 0; col < slot->bitmap.width; col++) {
        if (slot->bitmap.buffer[row * slot->bitmap.width + col] > 128) {
          XDrawPoint(dpy, win, gc, x_pos + col, y_pos + row);
        }
      }
    }

    pen_x += slot->advance.x >> 6; // Move pen
  }
}

// Quit and clean up
void quit(void) {
  // Free phrases
  for (int i = 0; config.phrases[i]; i++)
    free(config.phrases[i]);
  free(config.phrases);

  // Free paths
  free(config.pet_asset_dir);
  free(config.font_path);

  // Free keybinds
  free(config.keybinds);

  // Free animations
  if (pet.bubble_window)
    XDestroyWindow(dpy, pet.bubble_window);
  for (int i = 0; i < STATE_LAST; i++) {
    if (animations[i].frames) {
      for (int j = 0; j < animations[i].n_frames; j++) {
        XFreePixmap(dpy, animations[i].frames[j].pix);
        XFreePixmap(dpy, animations[i].frames[j].mask);
      }
      free(animations[i].frames);
    }
  }

  // Free FreeType2
  if (ft_face)
    FT_Done_Face(ft_face);
  if (ft_lib)
    FT_Done_FreeType(ft_lib);

  XCloseDisplay(dpy);
  exit(EXIT_SUCCESS);
}

// Pick random destination
void pick_random_destination(void) {
  int min_x = DEFAULT_WANDER_MARGIN;
  int min_y = DEFAULT_WANDER_MARGIN;
  int scaled_size = DEFAULT_ORIGINAL_SIZE * config.scale_factor;
  int max_x = scr_width - DEFAULT_WANDER_MARGIN - scaled_size;
  int max_y = scr_height - DEFAULT_WANDER_MARGIN - scaled_size;
  if (max_x < min_x)
    max_x = min_x;
  if (max_y < min_y)
    max_y = min_y;
  pet.target_x = min_x + (rand() % (max_x - min_x + 1));
  pet.target_y = min_y + (rand() % (max_y - min_y + 1));
  pet.wander_wait = 0;
}

// Set pet state
void set_pet_state(enum state new_state) {
  if (pet.state != new_state) {
    if (walking(pet.state) && walking(new_state)) {
      int current_frame = pet.current_frame;
      long frame_time = pet.frame_time;
      pet.state = new_state;
      pet.current_animation = &animations[new_state];
      if (pet.current_animation->n_frames > 0) {
        pet.current_frame = current_frame % pet.current_animation->n_frames;
        int dur = pet.current_animation->frames[pet.current_frame].duration;
        pet.frame_time = (frame_time < dur) ? frame_time : 0;
      } else {
        pet.current_frame = pet.frame_time = 0;
      }
    } else {
      pet.state = new_state;
      pet.current_animation = &animations[new_state];
      pet.current_frame = 0;
      pet.frame_time = 0;
    }
  } else if (pet.current_animation != &animations[new_state]) {
    pet.current_animation = &animations[new_state];
    if (!walking(new_state)) {
      pet.current_frame = pet.frame_time = 0;
    } else if (pet.current_animation->n_frames > 0) {
      pet.current_frame %= pet.current_animation->n_frames;
      int duration = pet.current_animation->frames[pet.current_frame].duration;
      if (pet.frame_time >= duration)
        pet.frame_time = 0;
    }
  }
}

// Draw speech bubble with FreeType2 text
void draw_bubble(void) {
  if (!pet.speech || !pet.bubble_window)
    return;
  XClearWindow(dpy, pet.bubble_window);
  GC gc = DefaultGC(dpy, scr);

  // Get text dimensions
  int text_width, text_height;
  ft_get_text_extents(pet.speech, &text_width, &text_height);

  // Draw text (offset to center vertically)
  ft_draw_text(pet.bubble_window, gc, pet.speech, 8, text_height + 6);
}

// Show speech bubble
void show_speech_bubble(const char *s) {
  if (!s)
    return;
  pet.speech = s;
  pet.speech_time = 0;

  if (!pet.bubble_window) {
    XSetWindowAttributes attr;
    attr.override_redirect = True;
    attr.background_pixel = WhitePixel(dpy, scr);
    attr.border_pixel = BlackPixel(dpy, scr);
    pet.bubble_window = XCreateWindow(
        dpy, root, 0, 0, 10, 10, 2, CopyFromParent, InputOutput, CopyFromParent,
        CWOverrideRedirect | CWBackPixel | CWBorderPixel, &attr);
    XSelectInput(dpy, pet.bubble_window, ExposureMask);
  }

  // Get text size with FreeType2
  int text_width, text_height;
  ft_get_text_extents(s, &text_width, &text_height);

  // Size bubble with padding
  int scaled_size = DEFAULT_ORIGINAL_SIZE * config.scale_factor;
  int bw = text_width + 16;  // 8px padding left/right
  int bh = text_height + 12; // 6px padding top/bottom
  int bx = CLAMP(pet.x + (scaled_size / 2) - bw / 2, 10, scr_width - 10 - bw);
  int by = CLAMP(pet.y - bh - 10, 10, scr_height - 10 - bh);

  XMoveResizeWindow(dpy, pet.bubble_window, bx, by, bw, bh);
  XMapWindow(dpy, pet.bubble_window);
  draw_bubble();
  XRaiseWindow(dpy, pet.bubble_window);
}

// Load animations
void load_animations(void) {
  memset(&xpm_attrs, 0, sizeof(xpm_attrs));
  xpm_attrs.valuemask = XpmSize;
  printf("Loading animations from: %s\n", config.pet_asset_dir);

  for (int i = 0; i < STATE_LAST; i++) {
    int frame_count = 0;
    char path[PATH_MAX];
    // Auto-detect frame count
    for (;;) {
      snprintf(path, sizeof(path), "%s/%s/%d.xpm", config.pet_asset_dir,
               animations[i].name, frame_count);
      FILE *f = fopen(path, "r");
      if (!f)
        break;
      fclose(f);
      frame_count++;
    }
    if (frame_count == 0) {
      fprintf(stderr, "Warning: No frames for '%s' (path: %s/%s)\n",
              animations[i].name, config.pet_asset_dir, animations[i].name);
      animations[i].frames = NULL;
      continue;
    }

    animations[i].frames = malloc(sizeof(struct frame) * frame_count);
    if (!animations[i].frames) {
      perror("malloc");
      exit(1);
    }

    for (int frame_index = 0; frame_index < frame_count; frame_index++) {
      snprintf(path, sizeof(path), "%s/%s/%d.xpm", config.pet_asset_dir,
               animations[i].name, frame_index);
      Pixmap pixmap = 0, mask = 0;
      int xpm_result =
          XpmReadFileToPixmap(dpy, root, path, &pixmap, &mask, &xpm_attrs);
      if (xpm_result != XpmSuccess) {
        fprintf(stderr, "Failed to load %s: XPM error %d\n", path, xpm_result);
        exit(EXIT_FAILURE);
      }

      // Scale assets
      Pixmap scaled_pix = scale_pixmap(dpy, pixmap, xpm_attrs.width,
                                       xpm_attrs.height, config.scale_factor);
      Pixmap scaled_mask = scale_mask(dpy, mask, xpm_attrs.width,
                                      xpm_attrs.height, config.scale_factor);
      XFreePixmap(dpy, pixmap);
      XFreePixmap(dpy, mask);

      animations[i].frames[frame_index] =
          (struct frame){.pix = scaled_pix,
                         .mask = scaled_mask,
                         .duration = config.frame_duration};
    }
    animations[i].n_frames = frame_count;
  }
}

// Create pet window
void create_window(void) {
  pet.x = scr_width / 2;
  pet.y = scr_height / 2;
  pet.subpixel_x = pet.x;
  pet.subpixel_y = pet.y;
  int scaled_size = DEFAULT_ORIGINAL_SIZE * config.scale_factor;

  XSetWindowAttributes attrs;
  attrs.override_redirect = True;
  pet.window = XCreateWindow(dpy, root, pet.x, pet.y, scaled_size, scaled_size,
                             0, CopyFromParent, InputOutput, CopyFromParent,
                             CWOverrideRedirect, &attrs);

  // Set initial image
  struct frame *f = &pet.current_animation->frames[0];
  XShapeCombineMask(dpy, pet.window, ShapeBounding, 0, 0, f->mask, ShapeSet);
  XSetWindowBackgroundPixmap(dpy, pet.window, f->pix);
  XSelectInput(dpy, pet.window,
               ButtonPressMask | ButtonReleaseMask | PointerMotionMask);
  XMapWindow(dpy, pet.window);
  XClearWindow(dpy, pet.window);
  XRaiseWindow(dpy, pet.window);
}

// Grab keybinds
void grab_keys(void) {
  unsigned int mods[] = {0, LockMask, Mod2Mask,
                         LockMask | Mod2Mask}; // Ignore caps/num lock
  for (int i = 0; i < config.keybind_count; i++) {
    KeySym sym = config.keybinds[i].sym;
    unsigned long base_mask = config.keybinds[i].mask;
    KeyCode code = XKeysymToKeycode(dpy, sym);
    if (code == NoSymbol)
      continue;
    for (unsigned j = 0; j < sizeof(mods) / sizeof(mods[0]); j++) {
      XGrabKey(dpy, code, base_mask | mods[j], root, True, GrabModeAsync,
               GrabModeAsync);
    }
  }
}

// Setup
void setup(void) {
  load_config();

  // Initialize display
  dpy = XOpenDisplay(NULL);
  if (!dpy) {
    fputs("Cannot open display\n", stderr);
    exit(1);
  }
  scr = DefaultScreen(dpy);
  scr_width = DisplayWidth(dpy, scr);
  scr_height = DisplayHeight(dpy, scr);
  root = RootWindow(dpy, scr);

  // Initialize FreeType2
  if (init_freetype() != 0) {
    fprintf(stderr, "Fatal: FreeType2 initialization failed\n");
    exit(1);
  }

  // Initialize pet
  srand((unsigned)time(NULL));
  load_animations();
  pet.current_frame = 0;
  pet.frame_time = 0;
  pet.frozen_time = 0;
  pet.happy_time = 0;
  pet.previous_state = IDLE;
  pet.dragging = False;
  pet.was_chasing = False;
  pet.was_frozen = False;
  pet.speech = NULL;
  pet.speech_time = 0;
  pet.bubble_window = 0;
  pet.chasing = False;
  pet.frozen = False;
  set_pet_state(IDLE);
  pick_random_destination();
  create_window();
  grab_keys();
  XSelectInput(dpy, root, KeyPressMask);
}

// Execute key function
void execute_key_func(KeyFunc func) {
  switch (func) {
  case FUNC_TOGGLE_CHASE:
    pet.chasing = !pet.chasing;
    if (pet.chasing) {
      pet.frozen = False;
      set_pet_state(E);
    } else {
      pick_random_destination();
    }
    break;
  case FUNC_TOGGLE_FREEZE:
    pet.frozen = !pet.frozen;
    if (pet.frozen) {
      set_pet_state(IDLE);
      pet.frozen_time = 0;
    }
    break;
  case FUNC_QUIT:
    quit();
    break;
  case FUNC_PRINT_HELLOWORLD:
    printf("keypress: hello world\n");
    break;
  default:
    break;
  }
}

// Handle key press
void on_key(XKeyEvent *ev) {
  KeySym sym = XLookupKeysym(ev, 0);
  unsigned long pressed_mask =
      ev->state & ~(LockMask | Mod2Mask); // Ignore locks

  for (int i = 0; i < config.keybind_count; i++) {
    if (config.keybinds[i].sym == sym &&
        config.keybinds[i].mask == pressed_mask) {
      execute_key_func(config.keybinds[i].func);
      break;
    }
  }
}

// Handle mouse press
void on_button_press(XButtonEvent *b) {
  if (b->button == Button1) {
    pet.was_chasing = pet.chasing;
    pet.was_frozen = pet.frozen;
    pet.dragging = True;
    pet.drag_offset_x = b->x;
    pet.drag_offset_y = b->y;
    pet.frozen = True;
    pet.chasing = False;
    set_pet_state(DRAGGED);
  } else if (b->button == Button3) {
    if (pet.state != HAPPY) {
      pet.previous_state = pet.state;
      set_pet_state(HAPPY);
      pet.happy_time = 0;
      if (config.phrase_count > 0) {
        int rand_idx = rand() % config.phrase_count;
        show_speech_bubble(config.phrases[rand_idx]);
      }
    }
  }
}

// Handle mouse release
void on_button_release(XButtonEvent *b) {
  if (b->button != Button1)
    return;
  pet.dragging = False;
  pet.frozen = pet.was_frozen;
  pet.chasing = pet.was_chasing;
  pet.subpixel_x = pet.x;
  pet.subpixel_y = pet.y;
  if (pet.was_frozen) {
    set_pet_state(IDLE);
    pet.frozen_time = 0;
  } else if (!pet.was_chasing) {
    pick_random_destination();
    set_pet_state(IDLE);
  } else
    set_pet_state(E);
}

// Handle mouse motion
void on_motion(XMotionEvent *m) {
  (void)m;
  if (!pet.dragging)
    return;
  get_mouse_pos();
  pet.x = mouse.x - pet.drag_offset_x;
  pet.y = mouse.y - pet.drag_offset_y;
  XMoveWindow(dpy, pet.window, pet.x, pet.y);
  if (pet.speech)
    show_speech_bubble(pet.speech);
}

// Move pet to target
void move_to(int target_x, int target_y) {
  double delta_x = target_x - pet.subpixel_x;
  double delta_y = target_y - pet.subpixel_y;
  double distance_squared = delta_x * delta_x + delta_y * delta_y;
  if (distance_squared < 4.0) {
    set_pet_state(IDLE);
    return;
  }

  enum state direction = find_octant((int)delta_x, (int)delta_y);
  double step_x = 0, step_y = 0;
  step(direction, &step_x, &step_y);

  if (distance_squared <= (config.pet_speed * config.pet_speed)) {
    pet.subpixel_x = target_x;
    pet.subpixel_y = target_y;
  } else {
    pet.subpixel_x += step_x;
    pet.subpixel_y += step_y;
  }

  pet.x = (int)(pet.subpixel_x + 0.5);
  pet.y = (int)(pet.subpixel_y + 0.5);
  set_pet_state(direction);
  XMoveWindow(dpy, pet.window, pet.x, pet.y);
}

// Wander behavior
void wander(void) {
  double delta_x = pet.target_x - pet.subpixel_x;
  double delta_y = pet.target_y - pet.subpixel_y;
  double distance_squared = delta_x * delta_x + delta_y * delta_y;

  if (distance_squared < 4.0) {
    if (pet.wander_wait <= 0) {
      pet.wander_wait =
          DEFAULT_WANDER_MIN_WAIT +
          (rand() % (DEFAULT_WANDER_MAX_WAIT - DEFAULT_WANDER_MIN_WAIT));
      set_pet_state(IDLE);
    } else {
      pet.wander_wait -= DEFAULT_PET_REFRESH;
      if (pet.wander_wait <= 0)
        pick_random_destination();
    }
    return;
  }

  move_to(pet.target_x, pet.target_y);
}

// Update animation frames
void update_animation(void) {
  if (!pet.current_animation || pet.current_animation->n_frames <= 0) {
    if (pet.state != IDLE)
      set_pet_state(IDLE);
    return;
  }

  pet.frame_time += DEFAULT_PET_REFRESH;
  struct frame *frame_ptr = &pet.current_animation->frames[pet.current_frame];
  if (pet.frame_time >= frame_ptr->duration) {
    pet.frame_time = 0;
    pet.current_frame++;
    if (pet.current_frame >= pet.current_animation->n_frames) {
      pet.current_frame =
          pet.current_animation->loop ? 0 : pet.current_animation->n_frames - 1;
    }
    frame_ptr = &pet.current_animation->frames[pet.current_frame];
    XShapeCombineMask(dpy, pet.window, ShapeBounding, 0, 0, frame_ptr->mask,
                      ShapeSet);
    XSetWindowBackgroundPixmap(dpy, pet.window, frame_ptr->pix);
    XClearWindow(dpy, pet.window);
  }
}

// Main loop
void run(void) {
  for (;;) {
    while (XPending(dpy)) {
      XEvent ev;
      XNextEvent(dpy, &ev);
      if (ev.type == KeyPress)
        on_key(&ev.xkey);
      else if (ev.type == ButtonPress && ev.xbutton.window == pet.window)
        on_button_press(&ev.xbutton);
      else if (ev.type == ButtonRelease && ev.xbutton.window == pet.window)
        on_button_release(&ev.xbutton);
      else if (ev.type == MotionNotify && ev.xmotion.window == pet.window)
        on_motion(&ev.xmotion);
      else if (ev.type == Expose && ev.xexpose.window == pet.bubble_window)
        draw_bubble();
    }

    // Update speech bubble
    if (pet.speech) {
      pet.speech_time += DEFAULT_PET_REFRESH;
      if (pet.speech_time >= DEFAULT_SPEECH_DURATION)
        hide_speech_bubble();
    }

    // Update happy state
    if (pet.state == HAPPY) {
      pet.happy_time += DEFAULT_PET_REFRESH;
      if (pet.happy_time >= DEFAULT_HAPPY_DURATION)
        set_pet_state(pet.previous_state);
    }

    // Update movement
    else if (!pet.dragging && !pet.frozen) {
      if (pet.chasing) {
        get_mouse_pos();
        move_to(mouse.x, mouse.y);
      } else {
        wander();
      }
    }

    // Update frozen state
    else if (pet.frozen) {
      pet.frozen_time += DEFAULT_PET_REFRESH;
      if (pet.frozen_time >= DEFAULT_SLEEP_DELAY && pet.state != SLEEPING) {
        set_pet_state(SLEEPING);
      }
    }

    // Update animation
    update_animation();

    // Keep windows on top
    XRaiseWindow(dpy, pet.window);
    if (pet.bubble_window)
      XRaiseWindow(dpy, pet.bubble_window);

    xsleep(DEFAULT_PET_REFRESH);
  }
}

int main(int argc, char **argv) {
  if (argc > 1) {
    printf("xpets " VERSION "\nWith FreeType2 font support\n");
    return 0;
  }
  setup();
  run();
  return 0;
}
